﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using SBS_Map_Editor.PBuffer;

namespace SBS_Map_Editor.Manip
{
    #region Enumerators
    [Flags]
    public enum AxisFlags : int
    {
        None = 0,
        X = (0x1 << 0),
        Y = (0x1 << 1),
        Z = (0x1 << 2),

        XY = X | Y,
        YX = Y | X,
        XZ = X | Z,
        ZX = Z | X,
        YZ = Y | Z,
        ZY = Z | Y,

        XYZ = X | Y | Z,

        All = XYZ
    }

    [Flags]
    public enum AxisDirections
    {
        Positive = (0x1 << 0),
        Negative = (0x1 << 1),

        All = Positive | Negative
    }

    [Flags]
    public enum TransformationMode
    {
        None,
        TranslationAxis = (0x1 << 0),
        TranslationPlane = (0x1 << 1),
        Rotation = (0x1 << 2),
        ScaleAxis = (0x1 << 3),
        ScalePlane = (0x1 << 4),
        ScaleUniform = (0x1 << 5)
    }

    public enum VectorSpace
    {
        World,
        Local
    }
    #endregion

    public sealed partial class Manipulator
    {
        private static short[] BoundIndices = new short[]
        {
            // x
            0, 1,   2, 3,   4, 5,   6, 7,
            // y
            0, 3,   1, 2,   4, 7,   5, 6,
            // z
            0, 4,   1, 5,   2, 6,   3, 7,
        };

        #region Extra Classes
        private class Input
        {
            #region Variables
            public Vector2 Start;
            public Vector2 End;
            public Vector2 Delta;

            public bool LeftButton;
            public bool LeftClick;
            public bool LeftRelease;

            public int X { get { return (int)End.X; } }
            public int Y { get { return (int)End.Y; } }
            #endregion

            #region Properties
            public float Length
            {
                get { return Delta.Length(); }
            }
            #endregion

            #region Constructor
            public Input()
            {
                Reset();
            }
            #endregion

            #region Helper Functions
            public void Reset()
            {
                MouseState mouse = Mouse.GetState();
                Start = End = new Vector2(mouse.X, mouse.Y);
                Delta = Vector2.Zero;
                LeftButton = LeftClick = (mouse.LeftButton == ButtonState.Pressed);
                LeftRelease = !LeftButton;
            }

            public void Cycle()
            {
                Start = End;

                MouseState mouse = Mouse.GetState();
                End = new Vector2(mouse.X, mouse.Y);

                Delta = End - Start;

                LeftClick = (LeftButton) ? (false) : (mouse.LeftButton == ButtonState.Pressed);
                LeftRelease = (LeftButton) ? (mouse.LeftButton == ButtonState.Released) : (false);
                LeftButton = mouse.LeftButton == ButtonState.Pressed;
            }
            #endregion
        }

        private delegate void DrawFunction(AxisFlags axis);
        private delegate void ManipFunction();

        private class DrawFunctions : Dictionary<TransformationMode,
            Dictionary<AxisFlags, DrawFunction>>
        {
            public DrawFunctions()
            {
                foreach (TransformationMode mode in Enum.GetValues(typeof(TransformationMode)))
                {
                    this[mode] = new Dictionary<AxisFlags, DrawFunction>();
                }
            }
        }

        private class ManipFunctions : Dictionary<TransformationMode,
            Dictionary<AxisFlags, ManipFunction>>
        {
            public ManipFunctions()
            {
                foreach (TransformationMode mode in Enum.GetValues(typeof(TransformationMode)))
                {
                    this[mode] = new Dictionary<AxisFlags, ManipFunction>();
                }
            }
        }
        #endregion

        #region Variables
        private IGraphicsDeviceService mGraphics;
        private VertexDeclaration mVertexDeclaration;
        private Effect mEffect;

        private PickBuffer mPickBuffer;
        private DepthStencilBuffer mDepthBuffer;

        private DrawFunctions mDrawFunctions;
        private ManipFunctions mManipFunctions;

        private ManipulatorSettings mSettings;

        private Input mInput;

        private ITransform mTransform;
        private TransformationMode mEnabledModes;
        private VectorSpace mVectorSpace;

        private bool mManipulating;

        private Stack<TransformState> mUndoStack;
        private Stack<TransformState> mRedoStack;

        private float mScale;

        // Properties
        public ITransform Transform
        {
            get { return mTransform; }
            set
            {
                if (mTransform == value)
                    return;

                mTransform = value;
                mManipulating = false;

                mInput.Reset();

                mUndoStack.Clear();
                mRedoStack.Clear();
            }
        }

        public TransformationMode EnabledModes
        {
            get { return mEnabledModes; }
            set
            {
                if (mEnabledModes == value)
                    return;

                if (mManipulating)
                    mRedoStack.Clear();

                mManipulating = false;
                mEnabledModes = value;
                ActiveMode = TransformationMode.None;
            }
        }

        public VectorSpace VectorSpace
        {
            get { return mVectorSpace; }
            set
            {
                if (mManipulating)
                    mRedoStack.Clear();

                mManipulating = false;
                mVectorSpace = value;
            }
        }

        private Matrix ViewMatrix
        {
            get { return (Camera != null) ? (Camera.ViewMatrix) : (Matrix.Identity); }
        }

        private Matrix ProjectionMatrix
        {
            get { return (Camera != null) ? (Camera.ProjectionMatrix) : (Matrix.Identity); }
        }

        private Matrix WorldMatrix
        {
            get
            {
                if (mTransform == null)
                    return Matrix.Identity;

                switch (mVectorSpace)
                {
                    case VectorSpace.Local:
                        {
                            return Matrix.CreateScale(mScale)
                                * Matrix.CreateFromQuaternion(mTransform.Rotation)
                                * Matrix.CreateTranslation(mTransform.Translation);
                        }

                    case VectorSpace.World:
                        {
                            return Matrix.CreateScale(mScale)
                                * Matrix.CreateTranslation(mTransform.Translation);
                        }

                    default:
                        return Matrix.Identity;
                }
            }
        }

        private Viewport Viewport
        {
            get
            {
                return (mGraphics.GraphicsDevice == null)
                    ? (new Viewport())
                    : (mGraphics.GraphicsDevice.Viewport);
            }
        }

        public bool Active
        {
            get { return (mTransform != null); }
        }

        public bool Focused
        {
            get
            {
                return (Active && (SelectedAxes != AxisFlags.None));
            }
        }

        public ICamera Camera { get; set; }
        public TransformationMode ActiveMode { get; set; }
        public AxisFlags SelectedAxes { get; set; }
        public int UndoStackSize { get { return mUndoStack.Count; } }
        public int RedoStackSize { get { return mRedoStack.Count; } }
        public ManipulatorSettings Settings { get; set; }
        #endregion

        #region Constructor
        public Manipulator(IGraphicsDeviceService graphics, ICamera camera)
        {
            mGraphics = graphics;
            Camera = camera;

            mInput = new Input();

            SelectedAxes = AxisFlags.None;
            ActiveMode = TransformationMode.None;
            mEnabledModes = TransformationMode.None;
            mVectorSpace = VectorSpace.World;

            mSettings = new ManipulatorSettings();
            mSettings.RestoreDefaults();

            mGraphics.DeviceCreated += new EventHandler(OnDeviceCreated);
            mGraphics.DeviceDisposing += new EventHandler(OnDeviceDisposing);
            if ((mGraphics.GraphicsDevice != null) && !mGraphics.GraphicsDevice.IsDisposed)
                OnDeviceCreated(this, null);

            mUndoStack = new Stack<TransformState>();
            mRedoStack = new Stack<TransformState>();

            mDrawFunctions = new DrawFunctions();
            mManipFunctions = new ManipFunctions();

            mDrawFunctions[TransformationMode.None][AxisFlags.X]
                = mDrawFunctions[TransformationMode.None][AxisFlags.Y]
                = mDrawFunctions[TransformationMode.None][AxisFlags.Z]
                    = delegate(AxisFlags axis)
                    {
                        Vector3 unit = GetUnitAxis(axis);
                        Primitives.DrawLine(mGraphics.GraphicsDevice, Vector3.Zero, unit);
                    };

            InitTranslations();
            InitRotation();
            InitScale();
        }
        #endregion

        #region Events
        private void OnDeviceCreated(object sender, EventArgs e)
        {
            GraphicsDevice device = mGraphics.GraphicsDevice;

            CompiledEffect ce = Effect.CompileEffectFromSource(EffectSource, new CompilerMacro[0],
                null, CompilerOptions.None, TargetPlatform.Windows);

            mEffect = new Effect(mGraphics.GraphicsDevice, ce.GetEffectCode(), CompilerOptions.None, null);

            mVertexDeclaration = new VertexDeclaration(device,
                new VertexElement[] { new VertexElement(0, 0, VertexElementFormat.Vector3,
                        VertexElementMethod.Default, VertexElementUsage.Position, 0) });

            int bbw = mGraphics.GraphicsDevice.PresentationParameters.BackBufferWidth;
            int bbh = mGraphics.GraphicsDevice.PresentationParameters.BackBufferHeight;

            mDepthBuffer = new DepthStencilBuffer(mGraphics.GraphicsDevice, bbw, bbh,
                mGraphics.GraphicsDevice.DepthStencilBuffer.Format);
        }

        private void OnDeviceDisposing(object sender, EventArgs e)
        {
            if ((mEffect != null) && !mEffect.IsDisposed)
                mEffect.Dispose();

            if ((mVertexDeclaration != null) && !mVertexDeclaration.IsDisposed)
                mVertexDeclaration.Dispose();

            mEffect = null;
            mVertexDeclaration = null;
        }
        #endregion

        #region Action Functions
        #region Update
        public void Update()
        {
            if (!Active)
                return;

            mInput.Cycle();

            Vector3 viewpos = Matrix.Invert(ViewMatrix).Translation;

            Matrix scaleview = Matrix.CreateLookAt(Vector3.UnitZ *
                Vector3.Distance(viewpos, mTransform.Translation),
                Vector3.Zero, Vector3.UnitY);

            Vector3 projstart = Viewport.Project(Vector3.Zero, ProjectionMatrix, scaleview, Matrix.Identity);
            Vector3 projend = Viewport.Project(Vector3.UnitX, ProjectionMatrix, scaleview, Matrix.Identity);

            mScale = 50.0f / Vector3.Subtract(projend, projstart).Length();

            if (!mManipulating)
                Pick();

            if (!mInput.LeftButton)
            {
                if (mManipulating)
                    mRedoStack.Clear();

                mManipulating = false;
            }
            else if (mInput.LeftClick)
            {
                if (SelectedAxes != AxisFlags.None)
                {
                    mManipulating = true;
                    mUndoStack.Push(new TransformState(mTransform));
                }
            }

            if (mManipulating && (mInput.Length > 0) && mManipFunctions[ActiveMode].ContainsKey(SelectedAxes))
            {
                mManipFunctions[ActiveMode][SelectedAxes]();
            }
        }

        private void Pick()
        {
            mPickBuffer.PushMatrix(MatrixMode.View, ViewMatrix);
            mPickBuffer.PushMatrix(MatrixMode.Projection, ProjectionMatrix);
            mPickBuffer.PushMatrix(MatrixMode.World, WorldMatrix);
            mPickBuffer.PushVertexDeclaration(mVertexDeclaration);

            foreach (TransformationMode mode in mDrawFunctions.Keys)
            {
                TransformationMode local_mode = mode;

                if ((mEnabledModes & local_mode) != local_mode)
                    continue;

                foreach (AxisFlags flags in mDrawFunctions[local_mode].Keys)
                {
                    AxisFlags local_flags = flags;

                    mPickBuffer.PushPickID(((uint)local_mode << 8) | (uint)flags);
                    mPickBuffer.QueueRenderFunction(delegate(GraphicsDevice device)
                    {
                        mDrawFunctions[local_mode][local_flags](local_flags);
                    });
                    mPickBuffer.PopPickID();
                }
            }

            mPickBuffer.PopVertexDeclaration();
            mPickBuffer.PopMatrix(MatrixMode.View);
            mPickBuffer.PopMatrix(MatrixMode.Projection);
            mPickBuffer.PopMatrix(MatrixMode.World);


            uint pick_id = mPickBuffer.Pick(mInput.X, mInput.Y);

            SelectedAxes = (AxisFlags)(pick_id & 0xFF);
            ActiveMode = (TransformationMode)((pick_id >> 8) & 0xFF);
        }
        #endregion

        #region Draw
        public void Draw()
        {
            if (!Active)
                return;

            GraphicsDevice device = mGraphics.GraphicsDevice;

            device.VertexDeclaration = mVertexDeclaration;

            mEffect.Parameters["View"].SetValue(ViewMatrix);
            mEffect.Parameters["Projection"].SetValue(ProjectionMatrix);

            if (mSettings.DrawBoundsEnabled && (mTransform is IBoundedTransform))
                DrawBounds((IBoundedTransform)mTransform);

            DepthStencilBuffer depth = device.DepthStencilBuffer;
            device.DepthStencilBuffer = mDepthBuffer;
            device.Clear(ClearOptions.DepthBuffer, Color.Black, 1f, 0);

            mEffect.Parameters["World"].SetValue(WorldMatrix);

            mEffect.Begin(SaveStateMode.SaveState);
            mEffect.CurrentTechnique.Passes[0].Begin();

            foreach (TransformationMode mode in mDrawFunctions.Keys)
            {
                if ((mEnabledModes & mode) != mode)
                    continue;

                foreach (AxisFlags flags in mDrawFunctions[mode].Keys)
                {
                    mEffect.Parameters["Color"].SetValue(GetAxisColor(mode, flags).ToVector3());
                    mEffect.CommitChanges();

                    mDrawFunctions[mode][flags](flags);
                }
            }

            mEffect.CurrentTechnique.Passes[0].End();
            mEffect.End();

            device.DepthStencilBuffer = depth;
        }

        private void DrawBounds(IBoundedTransform tform)
        {
            Vector3[] vtx = tform.Bounds.GetCorners();

            Color[] colors = new Color[]
            {
                mSettings.XAxisColor,
                mSettings.YAxisColor,
                mSettings.ZAxisColor,
            };

            mEffect.Parameters["World"].SetValue(Matrix.CreateScale(tform.Scale)
                * Matrix.CreateFromQuaternion(tform.Rotation)
                * Matrix.CreateTranslation(tform.Translation));

            mEffect.Begin();
            mEffect.CurrentTechnique.Passes[0].Begin();

            for (int i = 0, col = 0; i < BoundIndices.Length; i += 8, col++)
            {
                // set appropriate axis color on the effect
                mEffect.Parameters["Color"].SetValue(colors[col].ToVector3());
                mEffect.CommitChanges();

                // draw the four bounding lines for this axis
                mGraphics.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.LineList,
                    vtx, 0, 8, BoundIndices, i, 4);
            }

            mEffect.CurrentTechnique.Passes[0].End();
            mEffect.End();
        }
        #endregion
        #endregion

        #region Helper Functions
        private Vector3 GetUnitAxis(AxisFlags axes)
        {
            Vector3 unit = Vector3.Zero;

            if ((axes & AxisFlags.X) == AxisFlags.X)
                unit += Vector3.UnitX;
            if ((axes & AxisFlags.Y) == AxisFlags.Y)
                unit += Vector3.UnitY;
            if ((axes & AxisFlags.Z) == AxisFlags.Z)
                unit += Vector3.UnitZ;

            if (unit != Vector3.Zero)
                unit.Normalize();

            unit = ((mVectorSpace == VectorSpace.Local)
                       && (mTransform != null))
               ? (Vector3.TransformNormal(unit,
                   Matrix.CreateFromQuaternion(mTransform.Rotation)))
               : (unit);

            return unit;
        }

        private Plane GetPlane(AxisFlags axis)
        {
            Plane p = new Plane();

            switch (axis)
            {
                case AxisFlags.X:
                case AxisFlags.Y | AxisFlags.Z:
                    p.Normal = Vector3.UnitX;
                    break;
                case AxisFlags.Y:
                case AxisFlags.X | AxisFlags.Z:
                    p.Normal = Vector3.UnitY;
                    break;
                case AxisFlags.Z:
                case AxisFlags.X | AxisFlags.Y:
                    p.Normal = Vector3.UnitZ;
                    break;
            }

            if (mTransform == null)
                return p;

            p = (mVectorSpace == VectorSpace.Local) ?
                (Plane.Transform(p, Matrix.CreateFromQuaternion(mTransform.Rotation)
                * Matrix.CreateTranslation(mTransform.Translation))) :
                (Plane.Transform(p, Matrix.CreateTranslation(mTransform.Translation)));

            return p;
        }

        private Ray GetPickRay(Vector2 position)
        {
            Vector3 near = new Vector3(position, 0f);
            Vector3 far = new Vector3(position, 1f);

            near = Viewport.Unproject(near, ProjectionMatrix, ViewMatrix, Matrix.Identity);
            far = Viewport.Unproject(far, ProjectionMatrix, ViewMatrix, Matrix.Identity);

            Ray pickRay = new Ray(near, Vector3.Normalize(far - near));

            return pickRay;
        }

        public bool Undo()
        {
            if (!Active)
                return false;

            if (mUndoStack.Count == 0)
                return false;

            if (mManipulating)
            {
                mRedoStack.Clear();
                mManipulating = false;
            }

            mRedoStack.Push(new TransformState(mTransform));

            TransformState state = mUndoStack.Pop();
            state.Apply();

            return true;
        }

        public bool Redo()
        {
            if (!Active || mManipulating)
                return false;

            if (mRedoStack.Count == 0)
                return false;

            if (mManipulating)
            {
                mUndoStack.Clear();
                mManipulating = false;
            }

            mUndoStack.Push(new TransformState(mTransform));

            TransformState state = mRedoStack.Pop();
            state.Apply();

            return true;
        }

        private Color GetAxisColor(AxisFlags axis)
        {
            Color color = ((SelectedAxes & axis) == axis) ? (mSettings.SelectionColor) : (mSettings.GetAxisColor(axis));

            return color;
        }

        private Color GetAxisColor(TransformationMode mode, AxisFlags axis)
        {
            Color color = (ActiveMode == mode) ? (GetAxisColor(axis)) : (mSettings.GetAxisColor(axis));

            return color;
        }
        #endregion

        #region Effects
        private static string EffectSource =
    @"  float4x4 World;
        float4x4 View;
        float4x4 Projection;
        float3 Color;

        float4 VS(float4 Position : POSITION0) : POSITION0
        {
            return mul(Position, mul(World, mul(View, Projection)));
        }

        float4 PS(float4 Position : POSITION0) : COLOR0
        {
            return float4(Color, 1.0f);
        }

        Technique Color
        {
            Pass Color
            {
                CullMode = None;
                
                VertexShader = compile vs_1_1 VS();
                PixelShader = compile ps_1_1 PS();
            }
        }";
        #endregion
    }
}